Now, setting a 1,000 m threshold and a minimum sample of 5, we perform the clustering with DBSCAN for each time period. This analysis aims at exploring the evolution of crime hotspots over time.
On top of showing the centroids of each cluster, we attempt to plot the convex hulls of each cluster as well.
Code
# Group by cluster label and generate convex hulls (or other boundaries) for each cluster.def create_cluster_geometry(cluster_points):return cluster_points.unary_union.convex_hull# Apply the geometry generationclustered_geometries = LAcrime[LAcrime["label"] !=-1].groupby(["Year", "Month", "Crime Type", "label"]).apply(lambda x: create_cluster_geometry(x.geometry))# Convert the resulting geometries into a MultiPolygon if there are multiple disjoint areas.multipolygons = []crime_labels = []Year = []Month = []for (year, month, crime_type, label), geom in clustered_geometries.items():ifisinstance(geom, Polygon): multipolygons.append(geom) crime_labels.append((crime_type, label)) Year.append(year) Month.append(month)elifisinstance(geom, MultiPolygon): multipolygons.extend(list(geom)) crime_labels.extend([(crime_type, label)] *len(list(geom))) Year.append(year) Month.append(month)# Create a new GeoDataFrame to store the clusters' multipolygon geometries.LAcrime_clustered = gpd.GeoDataFrame({"geometry": multipolygons, "Crime Type Label": crime_labels, "Year": Year, "Month": Month}, crs = LAcrime.crs)LAcrime_clustered = LAcrime_clustered.to_crs(epsg =4326)
Set up the features for the base map consisting of LAPD reporting districts.
Code
# Generate 21 unique colors using a colormap.num_precs =21cmap = matplotlib.colormaps.get_cmap("tab20b")prec_values = LAPD_rd["PREC"].unique()prec_colors = { prec: cmap(i / num_precs) for i, prec inenumerate(sorted(prec_values))}# Convert RGBA to Hex for folium compatibility.prec_colors_hex = { prec: f'#{int(r*255):02x}{int(g*255):02x}{int(b*255):02x}'for prec, (r, g, b, _) in prec_colors.items()}# Define the style function.def style_function(feature): prec = feature["properties"]["PREC"] color = prec_colors_hex.get(prec, "#808080")return {"fillColor": color,"color": "black","weight": 1,"fillOpacity": 0.6, }
For each time period, a hotspot map is returned. The red transparent polygons represent the convex hulls of the violent crimes clusters, and the blue polygons represent the convex hulls of the property crimes clusters. The centroids of violent and property crimes clusters are represented by red and green dots respectively. Noises for violent and property crimes are represented by grey and blue dots respectively.
Code
def hotspot_map(year, month): temp = LAcrime[(LAcrime["Month"] == month) & (LAcrime["Year"] == year)]# Initialize a map m = folium.Map(location = [34.0522, -118.2437], zoom_start =9)# Add LAPD reporting districts. folium.GeoJson( LAPD_rd, style_function = style_function, tooltip = folium.GeoJsonTooltip(fields = ["PREC"], aliases = ["Precinct:"], localize =True), name ="LAPD Reporting Districts" ).add_to(m)# Add cluster polygons for each crime type.for idx, row in LAcrime_clustered[(LAcrime_clustered["Year"] == year) & (LAcrime_clustered["Month"] == month)].iterrows():# Access the tuple components explicitly. crime_type, label = row["Crime Type Label"]# Assign colors based on the crime type. color ="red"if crime_type =="violent"else"blue"# Create a descriptive name for each cluster layer. layer_name =f"{crime_type.capitalize()} Cluster {label}"# Add the geometry to the Folium map. folium.GeoJson( row["geometry"], style_function =lambda feature, color = color: {"fillColor": color,"color": color,"weight": 1,"fillOpacity": 0.25, }, tooltip =f"Crime Type: {crime_type}, Cluster: {label}", name = layer_name ).add_to(m)# Step 4: Plot noise points and centroids for clusters.for crime_type, color, marker_color in [("violent", "grey", "red"), ("property", "blue", "green")]: crime_data = temp[temp["Crime Type"] == crime_type]# Plot noise points. noise_points = crime_data[crime_data["label"] ==-1]for _, row in noise_points.iterrows(): folium.CircleMarker( [row["LAT"], row["LON"]], radius =2, color = color, fill =True, fill_color = color, tooltip =f'Noise Point: {crime_type}', name =f"{crime_type.capitalize()} Noise Points" ).add_to(m)# Plot centroids for each cluster.for label in crime_data["label"].unique():if label ==-1:continue cluster_data = crime_data[crime_data["label"] == label] centroid_x = cluster_data["LON"].mean() centroid_y = cluster_data["LAT"].mean() folium.CircleMarker( [centroid_y, centroid_x], radius =2, color = marker_color, fill =True, fill_color = marker_color, tooltip =f"{crime_type} Cluster {label} Centroid", name =f"{crime_type.capitalize()} Cluster {label} Centroid" ).add_to(m)# Add Layer Control to toggle layers. folium.LayerControl().add_to(m)# Return the map.return pn.panel(m, height =500, width =800)
As a quick example, let’s look at the hotspot maps for January 2018, 2019, and 2020.